package alibaba

import (
	"context"
	"fmt"
	"os"
	"strconv"
	"strings"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/require"

	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/protocol"
	pb "golang.aliyun-inc.com/agit/satellite-proto/go"
)

func Test_handleNormal(t *testing.T) {
	for _, tc := range []struct {
		desc    string
		ref     string
		oldOid  string
		newOID  string
		wantErr bool
	}{
		{
			desc:    "handle normal command",
			ref:     "refs/heads/master",
			oldOid:  "0d3a6c9478483619e751163b8848439aebdb5722",
			newOID:  "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			wantErr: false,
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			hook := NewCodeupHook(os.Stdin, os.Stdout, os.Stderr, nil, nil, true, "Codeup", nil, nil)
			command := &protocol.Command{}
			command.SetIntendedCommand(protocol.GitCommand{
				OldOID: tc.oldOid,
				NewOID: tc.newOID,
				Ref:    tc.ref,
			})
			pbCmd, err := hook.handler.handleNormal(command)
			expectedVote := &pb.Command{
				Refname:    []byte(tc.ref),
				OldOid:     tc.oldOid,
				NewOid:     tc.newOID,
				SkipVoting: false,
			}
			if tc.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, pbCmd, expectedVote)
		})
	}
}

func Test_handleAGitFlow(t *testing.T) {
	os.Setenv("GL_ID", "user-15744")
	os.Setenv("GL_REPOSITORY", "dyrone/dyrone")
	os.Setenv("GL_REPOSITORY_ID", "34837")
	os.Setenv("GL_USERNAME", "dyroneteng@rdc.onaliyun.com")

	controller := gomock.NewController(t)
	defer controller.Finish()
	mockAPI := NewMockAPI(controller)
	mockedUploader := User{
		ID:       15744,
		Name:     "dyroneteng",
		Username: "dyroneteng@rdc.onaliyun.com",
		State:    "active",
	}
	mockedUserOther := User{
		ID:       178872,
		Name:     "moweng.xx",
		Username: "moweng.xx@rdc.onaliyun.com",
		State:    "active",
	}
	mockedMergeRequestResp := &MergeRequestResp{
		ID:           834,
		Iid:          1,
		ProjectID:    34837,
		Author:       mockedUploader,
		Assignee:     []User{mockedUploader},
		DetailURL:    "https://daily-codeup.aliyun.com/5f3e418e6207a1a8b17f9257/dyrone/dyrone/merge_request/834",
		SourceBranch: "98df15ee518f84592b95f04cd97816723157b185",
	}
	mockedMergeRequestRespForUpdate := &MergeRequestResp{
		ID:        834,
		Iid:       1,
		ProjectID: 34837,
		Author:    mockedUploader,
		Assignee:  []User{mockedUploader},
		DetailURL: "https://daily-codeup.aliyun.com/5f3e418e6207a1a8b17f9257/dyrone/dyrone/merge_request/834",
		RefOldOid: "98df15ee518f84592b95f04cd97816723157b185",
	}
	// mock for CreateMR
	mockAPI.EXPECT().CreateMR(gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, params *AGitFlowParams) (resp *MergeRequestResp, err error) {
			switch params.SourceBranch {
			case "create/new":
				resp = mockedMergeRequestResp
			case "create/update":
				resp = mockedMergeRequestRespForUpdate
			case "create/fail":
				err = fmt.Errorf("codeup create mr failed")
			case "create/ok/with/warnings":
				tmp := *mockedMergeRequestResp
				tmp.WarningMessage = "this\nis\na\nwarning"
				resp = &tmp
			default:
				err = fmt.Errorf("unsupported source branch %q", params.SourceBranch)
			}
			return
		},
	).AnyTimes()
	// mock for QueryMR
	mockAPI.EXPECT().QueryMR(gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, mrID string) (resp []*MergeRequestResp, err error) {
			switch mrID {
			case "author-ok", "update-mr-fail":
				resp = append(resp, mockedMergeRequestResp)
			case "assignee-ok":
				tmp := *mockedMergeRequestResp
				tmp.Author = mockedUserOther
				tmp.Assignee = append(tmp.Assignee, mockedUploader)
				resp = append(resp, &tmp)
			case "permission-denied":
				tmp := *mockedMergeRequestResp
				tmp.Author = mockedUserOther
				tmp.Assignee = []User{mockedUserOther}
				resp = append(resp, &tmp)
			case "query-mr-fail":
				err = fmt.Errorf("codeup update mr failed")
			}
			return
		},
	).AnyTimes()
	// mock for UpdateMR
	mockAPI.EXPECT().UpdateMR(gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, params *AGitFlowParams) (resp *MergeRequestResp, err error) {
			switch params.ID {
			case "author-ok":
				resp = mockedMergeRequestRespForUpdate
			case "assignee-ok":
				tmp := *mockedMergeRequestRespForUpdate
				tmp.Author = mockedUserOther
				resp = &tmp
			case "update-mr-fail":
				err = fmt.Errorf("codeup update mr failed")
			default:
				err = fmt.Errorf("unsupported mr %q", params.ID)
			}
			return
		},
	).AnyTimes()

	for _, tc := range []struct {
		desc          string
		ref           string
		oldOID        string
		newOID        string
		expectStatus  string
		expectRef     string
		expectNewRef  string
		expectOldOId  string
		expectNewOID  string
		expectOptions []string
		wantErr       bool
		reason        string
		pushOpt       map[string]string
	}{
		{
			desc:         "agitflow create mr success",
			oldOID:       "0d3a6c9478483619e751163b8848439aebdb5722",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for/master/create/new",
			expectStatus: "ok",
			expectRef:    "refs/for/master/create/new",
			expectNewRef: "refs/merge-requests/834/head",
			expectOldOId: "0000000000000000000000000000000000000000",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			expectOptions: []string{
				"option refname refs/merge-requests/834/head",
				"option old-oid 0000000000000000000000000000000000000000",
				"option new-oid 8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			},
			wantErr: false,
		},
		{
			desc:         "agitflow create mr update success",
			oldOID:       "98df15ee518f84592b95f04cd97816723157b185",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for/master/create/update",
			expectStatus: "ok",
			expectRef:    "refs/for/master/create/update",
			expectNewRef: "refs/merge-requests/834/head",
			expectOldOId: "98df15ee518f84592b95f04cd97816723157b185",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			expectOptions: []string{
				"option refname refs/merge-requests/834/head",
				"option old-oid 0000000000000000000000000000000000000000",
				"option new-oid 8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			},
			wantErr: false,
		},
		{
			desc:         "agitflow create mr success with warnings",
			oldOID:       "0d3a6c9478483619e751163b8848439aebdb5722",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for/master/create/ok/with/warnings",
			expectStatus: "ok",
			expectRef:    "refs/for/master/create/ok/with/warnings",
			expectNewRef: "refs/merge-requests/834/head",
			expectOldOId: "0000000000000000000000000000000000000000",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			expectOptions: []string{
				"option refname refs/merge-requests/834/head",
				"option old-oid 0000000000000000000000000000000000000000",
				"option new-oid 8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			},
			wantErr: false,
		},
		{
			desc:         "agitflow create mr with invalid agitflow operation",
			oldOID:       "0d3a6c9478483619e751163b8848439aebdb5722",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/invalid/master",
			expectStatus: "ng",
			expectRef:    "refs/invalid/master",
			expectOldOId: "0d3a6c9478483619e751163b8848439aebdb5722",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			reason:       `resolve agitflow params from command failed: illegal ref "refs/invalid/master" for agitflow`,
			wantErr:      true,
		},
		{
			desc:         "agitflow create mr fail",
			oldOID:       "0d3a6c9478483619e751163b8848439aebdb5722",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for/master/create/fail",
			expectStatus: "ng",
			expectRef:    "refs/for/master/create/fail",
			expectOldOId: "0d3a6c9478483619e751163b8848439aebdb5722",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			reason:       "create MR failed",
			wantErr:      true,
		},
		{
			desc:         "agitflow update mr success: uploader is author",
			oldOID:       "98df15ee518f84592b95f04cd97816723157b185",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for-review/author-ok",
			expectStatus: "ok",
			expectRef:    "refs/for-review/author-ok",
			expectNewRef: "refs/merge-requests/author-ok/head",
			expectOldOId: "98df15ee518f84592b95f04cd97816723157b185",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			pushOpt:      map[string]string{"oldoid": "98df15ee518f84592b95f04cd97816723157b185"},
			expectOptions: []string{
				"option refname refs/merge-requests/834/head",
				"option old-oid 0000000000000000000000000000000000000000",
				"option new-oid 8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			},
			wantErr: false,
		},
		{
			desc:         "agitflow update mr success: uploader is assignee",
			oldOID:       "98df15ee518f84592b95f04cd97816723157b185",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for-review/assignee-ok",
			expectStatus: "ok",
			expectRef:    "refs/for-review/assignee-ok",
			expectNewRef: "refs/merge-requests/assignee-ok/head",
			expectOldOId: "98df15ee518f84592b95f04cd97816723157b185",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			pushOpt:      map[string]string{"oldoid": "98df15ee518f84592b95f04cd97816723157b185"},
			expectOptions: []string{
				"option refname refs/merge-requests/834/head",
				"option old-oid 0000000000000000000000000000000000000000",
				"option new-oid 8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			},
			wantErr: false,
		},
		{
			desc:         "agitflow update mr: permission denied",
			oldOID:       "0d3a6c9478483619e751163b8848439aebdb5722",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for-review/permission-denied",
			expectStatus: "ng",
			expectRef:    "refs/for-review/permission-denied",
			expectOldOId: "0d3a6c9478483619e751163b8848439aebdb5722",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			reason:       "update MR failed",
			wantErr:      true,
		},
		{
			desc:         "agitflow update mr: query mr failed",
			oldOID:       "0d3a6c9478483619e751163b8848439aebdb5722",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for-review/query-mr-fail",
			expectStatus: "ng",
			expectRef:    "refs/for-review/query-mr-fail",
			expectOldOId: "0d3a6c9478483619e751163b8848439aebdb5722",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			reason:       "update MR failed",
			wantErr:      true,
		},
		{
			desc:         "agitflow update mr: update mr failed",
			oldOID:       "0d3a6c9478483619e751163b8848439aebdb5722",
			newOID:       "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			ref:          "refs/for-review/update-mr-fail",
			expectStatus: "ng",
			expectRef:    "refs/for-review/update-mr-fail",
			expectOldOId: "0d3a6c9478483619e751163b8848439aebdb5722",
			expectNewOID: "8ebb9f0b8ca7c32ab60357a8f4a5f18e8706f1f4",
			reason:       `update MR failed`,
			wantErr:      true,
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			detector, _ := NewGlobsDetector([]string{"refs/for", "refs/for-review/", "refs/drafts/**"}, []string{"refs/tags/"}, []string{"refs/heads"}, []string{"refs/merge-requests/"})
			router, _ := NewCodeupRouter(detector)
			hook := NewCodeupHook(os.Stdin, os.Stdout, nil, mockAPI, nil, true, "Codeup", router, nil)
			for k, v := range tc.pushOpt {
				*hook.pushopts = append(*hook.pushopts, protocol.PushOption{Name: k, Value: v})
			}
			command := &protocol.Command{
				GitCommand: protocol.GitCommand{
					OldOID: tc.oldOID,
					NewOID: tc.newOID,
					Ref:    tc.ref,
				},
			}
			pbCmd, err := hook.handler.handleAgitFlow(context.Background(), command)
			expectedVote := &pb.Command{
				Refname:    []byte(tc.expectNewRef),
				OldOid:     tc.expectOldOId,
				NewOid:     tc.expectNewOID,
				SkipVoting: true,
			}
			if tc.wantErr {
				require.Equal(t, tc.reason, command.GetReason())
				return
			}
			require.NoError(t, err)
			require.Equal(t, pbCmd.Refname, expectedVote.Refname)
			require.Equal(t, pbCmd.NewOid, expectedVote.NewOid)
			require.Equal(t, pbCmd.OldOid, expectedVote.OldOid)
			require.Equal(t, pbCmd.SkipVoting, expectedVote.SkipVoting)
		})
	}
}

func Test_handleTrunkBase(t *testing.T) {
	const (
		CREATE_MR_SUCCESS_OID   = "df117b65866370ba9d7e66ee4c2ed1e0a6059cfb"
		CREATE_MR_NO_CHANGE_OID = "2487439bd53ce572597556cbb1cd0b7bc8c316de"
		UPDATE_MR_DIVERGE_OID   = "8a5d332cef68ad537aab5b30113511e79d95b79d"
		OLD_OID                 = "9464c5b547c0bf7e7174e5743b3e28d155b62923"
		CLIENT                  = "CLIENT"
		OPENED                  = "opened"
	)

	os.Setenv("GL_ID", "user-15744")
	os.Setenv("GL_REPOSITORY", "dyrone/dyrone")
	os.Setenv("GL_REPOSITORY_ID", "34837")
	os.Setenv("GL_USERNAME", "dyroneteng@rdc.onaliyun.com")
	controller := gomock.NewController(t)
	defer controller.Finish()
	mockAPI := NewMockAPI(controller)
	mockChecker := NewMockDivergeChecker(controller)
	mockedUploader := User{
		ID:       15744,
		Name:     "dyroneteng",
		Username: "dyroneteng@rdc.onaliyun.com",
		State:    "active",
	}
	mockedUserOther := User{
		ID:       178872,
		Name:     "moweng.xx",
		Username: "moweng.xx@rdc.onaliyun.com",
		State:    "active",
	}
	mockedCreateMergeRequestResp := &MergeRequestResp{
		ID:             11111,
		Iid:            1,
		ProjectID:      34837,
		Author:         mockedUploader,
		Assignee:       []User{mockedUploader},
		UpdatedByID:    mockedUploader.ID,
		DetailURL:      "https://daily-codeup.aliyun.com/5f3e418e6207a1a8b17f9257/dyrone/dyrone/merge_request/11111",
		SourceBranch:   OLD_OID,
		TargetBranch:   "unit_test/single_existing_mr",
		CreationMethod: CLIENT,
		State:          OPENED,
	}
	mockedMergeRequestResp := &MergeRequestResp{
		ID:             11111,
		Iid:            1,
		ProjectID:      34837,
		Author:         mockedUploader,
		Assignee:       []User{mockedUploader},
		UpdatedByID:    mockedUploader.ID,
		DetailURL:      "https://daily-codeup.aliyun.com/5f3e418e6207a1a8b17f9257/dyrone/dyrone/merge_request/11111",
		SourceBranch:   OLD_OID,
		TargetBranch:   "unit_test/single_existing_mr",
		RefOldOid:      OLD_OID,
		CreationMethod: CLIENT,
		State:          OPENED,
	}
	mockedAssigneeMergeRequestResp := &MergeRequestResp{
		ID:             22222,
		Iid:            1,
		ProjectID:      34837,
		Author:         mockedUserOther,
		Assignee:       []User{mockedUploader, mockedUserOther},
		UpdatedByID:    mockedUserOther.ID,
		DetailURL:      "https://daily-codeup.aliyun.com/5f3e418e6207a1a8b17f9257/dyrone/dyrone/merge_request/22222",
		RefOldOid:      OLD_OID,
		SourceBranch:   OLD_OID,
		TargetBranch:   "unit_test/query_reviewer",
		CreationMethod: CLIENT,
		State:          OPENED,
	}
	mockedNonPermitMergeRequestResp := &MergeRequestResp{
		ID:          44444,
		Iid:         1,
		ProjectID:   34837,
		Author:      mockedUserOther,
		Assignee:    []User{mockedUserOther},
		UpdatedByID: mockedUserOther.ID,
		DetailURL:   "https://daily-codeup.aliyun.com/5f3e418e6207a1a8b17f9257/dyrone/dyrone/merge_request/44444",
		RefOldOid:   OLD_OID,
	}
	mockAPI.EXPECT().CreateMR(gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, params *AGitFlowParams) (resp *MergeRequestResp, err error) {
			switch params.NewOID {
			case CREATE_MR_SUCCESS_OID:
				resp = mockedCreateMergeRequestResp
			case CREATE_MR_NO_CHANGE_OID:
				err = fmt.Errorf("无法完成此操作！存在进行中的合并请求")
			}
			return
		}).AnyTimes()

	mockAPI.EXPECT().QueryMRs(gomock.Any(), gomock.Any(), gomock.Any(), "unit_test/no_existing_mr").DoAndReturn(
		func(ctx context.Context, projectID, authorID, targetBranch string) ([]*MergeRequestResp, error) {
			return nil, nil
		}).AnyTimes()

	mockAPI.EXPECT().QueryMRs(gomock.Any(), gomock.Any(), gomock.Any(), "unit_test/single_existing_mr").DoAndReturn(
		func(ctx context.Context, projectID, authorID, targetBranch string) ([]*MergeRequestResp, error) {
			return []*MergeRequestResp{
				mockedMergeRequestResp,
			}, nil
		}).AnyTimes()
	mockAPI.EXPECT().QueryMRs(gomock.Any(), gomock.Any(), gomock.Any(), "unit_test/multi_existing_mr").DoAndReturn(
		func(ctx context.Context, projectID, authorID, targetBranch string) ([]*MergeRequestResp, error) {
			return []*MergeRequestResp{
				mockedMergeRequestResp,
				mockedAssigneeMergeRequestResp,
			}, nil
		}).AnyTimes()

	mockAPI.EXPECT().QueryMRs(gomock.Any(), gomock.Any(), gomock.Any(), "unit_test/query_reviewer").DoAndReturn(
		func(ctx context.Context, projectID, authorID, targetBranch string) ([]*MergeRequestResp, error) {
			return []*MergeRequestResp{
				mockedAssigneeMergeRequestResp,
			}, nil
		}).AnyTimes()

	mockAPI.EXPECT().QueryMR(gomock.Any(), "11111").DoAndReturn(
		func(ctx context.Context, mrID string) ([]*MergeRequestResp, error) {
			return []*MergeRequestResp{mockedMergeRequestResp}, nil
		},
	).AnyTimes()

	mockAPI.EXPECT().QueryMR(gomock.Any(), "22222").DoAndReturn(
		func(ctx context.Context, mrID string) ([]*MergeRequestResp, error) {
			return []*MergeRequestResp{mockedAssigneeMergeRequestResp}, nil
		},
	).AnyTimes()

	mockAPI.EXPECT().QueryMR(gomock.Any(), "33333").DoAndReturn(
		func(ctx context.Context, mrID string) ([]*MergeRequestResp, error) {
			return []*MergeRequestResp{mockedMergeRequestResp}, nil
		},
	).AnyTimes()

	mockAPI.EXPECT().QueryMR(gomock.Any(), "44444").DoAndReturn(
		func(ctx context.Context, mrID string) ([]*MergeRequestResp, error) {
			return []*MergeRequestResp{mockedNonPermitMergeRequestResp}, nil
		},
	).AnyTimes()

	mockAPI.EXPECT().UpdateMR(gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, params *AGitFlowParams) (*MergeRequestResp, error) {
			switch params.ID {
			case "11111":
				newMockedResp := *mockedMergeRequestResp
				newMockedResp.RefOldOid = "9464c5b547c0bf7e7174e5743b3e28d155b62923"
				return &newMockedResp, nil
			case "22222":
				return mockedAssigneeMergeRequestResp, nil
			case "33333":
				return nil, fmt.Errorf("The remote MR #33333 has been updated.")
			}
			return nil, nil
		},
	).AnyTimes()
	mockChecker.EXPECT().checkDiverge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
		func(ctx context.Context, command *protocol.Command, params *AGitFlowParams, mr *MergeRequestResp) error {
			if params.TargetBranch != "" && mr.TargetBranch != params.TargetBranch {
				return fmt.Errorf("MR #%d points to %s, but you pushed to another branch: %s", mr.ID, mr.TargetBranch, params.TargetBranch)
			}
			if params.OldCommit != "" && !strings.HasPrefix(mr.RefOldOid, params.OldCommit) {
				return fmt.Errorf("MR is diverged with this push")
			}
			if params.UserID == strconv.Itoa(mr.UpdatedByID) {
				return nil
			}
			if command.NewOID == UPDATE_MR_DIVERGE_OID && mr.RefOldOid == OLD_OID {
				return fmt.Errorf("MR is diverged with this push")
			}
			return nil
		},
	).AnyTimes()

	for _, tc := range []struct {
		desc         string
		ref          string
		oldOID       string
		newOID       string
		expectStatus string
		expectRef    string
		expectNewRef string
		expectOldOId string
		expectNewOID string
		pushOpt      map[string]string
		wantErr      bool
		reason       string
		skipVoting   bool
	}{
		{
			desc:         "review=new create MR success",
			ref:          "refs/heads/master",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/master",
			expectNewRef: "refs/merge-requests/11111/head",
			expectOldOId: NullSHA1,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt:      map[string]string{"review": "new"},
			wantErr:      false,
			reason:       "",
			skipVoting:   true,
		},
		{
			desc:         "local not changed, review=new create MR failed",
			ref:          "refs/heads/master",
			oldOID:       CREATE_MR_NO_CHANGE_OID,
			newOID:       CREATE_MR_NO_CHANGE_OID,
			expectRef:    "refs/heads/master",
			expectNewRef: "",
			expectOldOId: NullSHA1,
			expectNewOID: "",
			pushOpt:      map[string]string{"review": "new"},
			wantErr:      true,
			reason:       "create MR failed",
			skipVoting:   true,
		},
		{
			desc: "review=smart create MR success",
			// 需要本地有unit_test/no_existing_mr这个分支
			ref:          "refs/heads/unit_test/no_existing_mr",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/no_existing_mr",
			expectNewRef: "refs/merge-requests/11111/head",
			expectOldOId: NullSHA1,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt:      map[string]string{"review": "smart"},
			wantErr:      false,
			reason:       "",
			skipVoting:   true,
		},
		{
			desc: "review=smart update MR success",
			// 需要本地有unit_test/single_existing_mr这个分支
			ref:          "refs/heads/unit_test/single_existing_mr",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/unit_test/single_existing_mr",
			expectNewRef: "refs/merge-requests/11111/head",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt:      map[string]string{"review": "smart"},
			wantErr:      false,
			reason:       "",
			skipVoting:   true,
		},
		{
			desc: "review=smart find multiple MR, update MR failed",
			// 需要本地有unit_test/multi_existing_mr这个分支
			ref:          "refs/heads/unit_test/multi_existing_mr",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "",
			expectNewRef: "",
			expectOldOId: NullSHA1,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt:      map[string]string{"review": "smart"},
			wantErr:      true,
			reason:       "unable to determine which mr to update",
			skipVoting:   true,
		},
		{
			desc:         "review=smart reviewer update MR success",
			ref:          "refs/heads/unit_test/query_reviewer",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/unit_test/query_reviewer",
			expectNewRef: "refs/merge-requests/22222/head",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt:      map[string]string{"review": "smart"},
			wantErr:      false,
			reason:       "",
			skipVoting:   true,
		},
		{
			desc:         "write permission review=no push success",
			ref:          "refs/heads/master",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/master",
			expectNewRef: "refs/heads/master",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt:      map[string]string{"review": "no"},
			wantErr:      false,
			reason:       "",
			skipVoting:   false,
		},
		{
			desc:         "author review=<mrid> update MR success",
			ref:          "refs/heads/unit_test/single_existing_mr",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/master",
			expectNewRef: "refs/merge-requests/11111/head",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt: map[string]string{
				"review": "11111",
			},
			wantErr:    false,
			reason:     "",
			skipVoting: true,
		},
		{
			desc:         "reviewer review=<mrid> update MR success",
			ref:          "refs/heads/unit_test/query_reviewer",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/unit_test/query_reviewer",
			expectNewRef: "refs/merge-requests/22222/head",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt: map[string]string{
				"review": "22222",
			},
			wantErr:    false,
			reason:     "",
			skipVoting: true,
		},
		{
			desc:         "reviewer review=<mrid> update diverged MR failed",
			ref:          "refs/heads/master",
			oldOID:       OLD_OID,
			newOID:       UPDATE_MR_DIVERGE_OID,
			expectRef:    "refs/heads/master",
			expectNewRef: "refs/merge-requests/22222/head",
			expectOldOId: OLD_OID,
			expectNewOID: UPDATE_MR_DIVERGE_OID,
			pushOpt: map[string]string{
				"review": "22222",
			},
			wantErr:    true,
			reason:     "MR is diverged with this push",
			skipVoting: true,
		},
		{
			desc:         "other guy review=<mrid> update MR failed",
			ref:          "refs/heads/master",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/master",
			expectNewRef: "refs/merge-requests/44444/head",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt: map[string]string{
				"review": "44444",
			},
			wantErr:    true,
			reason:     "无权限操作MR或对应MR不存在",
			skipVoting: true,
		},
		{
			desc:         "author review=<mrid> old-oid=<old-oid> update MR success",
			ref:          "refs/heads/master",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/master",
			expectNewRef: "refs/merge-requests/11111/head",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt: map[string]string{
				"review":  "11111",
				"old-oid": OLD_OID,
			},
			wantErr:    false,
			reason:     "",
			skipVoting: true,
		},
		{
			desc:         "author review=<mrid> old-oid=<wrong-old-oid> update MR failed",
			ref:          "refs/heads/master",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/master",
			expectNewRef: "refs/merge-requests/33333/head",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt: map[string]string{
				"review":  "33333",
				"old-oid": "abcdefg123456",
			},
			wantErr:    true,
			reason:     "MR is diverged with this push",
			skipVoting: true,
		},
		{
			desc:         "reviewer review=<mrid> old-oid=<old-oid> update MR success",
			ref:          "refs/heads/unit_test/query_reviewer",
			oldOID:       OLD_OID,
			newOID:       CREATE_MR_SUCCESS_OID,
			expectRef:    "refs/heads/unit_test/query_reviewer",
			expectNewRef: "refs/merge-requests/22222/head",
			expectOldOId: OLD_OID,
			expectNewOID: CREATE_MR_SUCCESS_OID,
			pushOpt: map[string]string{
				"review":  "22222",
				"old-oid": OLD_OID,
			},
			wantErr:    false,
			reason:     "",
			skipVoting: true,
		},
	} {
		t.Run(tc.desc, func(t *testing.T) {
			hook := NewCodeupHook(os.Stdin, os.Stdout, nil, mockAPI, nil, true, "Codeup", nil, mockChecker)
			command, err := protocol.NewCommand(protocol.GitCommand{
				OldOID: tc.oldOID,
				NewOID: tc.newOID,
				Ref:    tc.ref,
			})
			require.NoError(t, err)
			pbCmd, err := hook.handler.handleTrunkBase(context.Background(), command, tc.pushOpt)
			expectedVote := &pb.Command{
				Refname:    []byte(tc.expectNewRef),
				OldOid:     tc.expectOldOId,
				NewOid:     tc.expectNewOID,
				SkipVoting: tc.skipVoting,
			}
			if tc.wantErr {
				require.Equal(t, tc.reason, command.GetReason())
				return
			}
			require.NoError(t, err)
			require.Equal(t, expectedVote.Refname, pbCmd.Refname)
			require.Equal(t, expectedVote.NewOid, pbCmd.NewOid)
			require.Equal(t, expectedVote.OldOid, pbCmd.OldOid)
			require.Equal(t, expectedVote.SkipVoting, pbCmd.SkipVoting)
		})
	}
}
